﻿//#include rfm

(function() {

var _validators = [],
    _guid = 0,
    _lastClickedButtonId = null;

function _ignore(input) {
    if (input.disabled) {
        return true;
    }
    while (input) {
        if (input.nodeType == 1 && input.getAttribute('data-ignore') == 'true') {
            return true;
        }
        input = input.parentNode;
    }
    return false;
}

function _radioCheckListSelection(input) {
	var checked = false,
		fieldsWithSameName = input.form.querySelectorAll('input[name="' + input.name + '"]'),
		i = fieldsWithSameName.length - 1,
		oneOption = i == 0;
	while (i > -1) {
		checked = fieldsWithSameName[i].checked;
		if (checked) {
			break;
		}
		i--;
	}
	return {
	    hasSelection: checked,
	    oneOption: oneOption
	};
}

function _collectValues(context, buffer) {
    var inputs = [].slice.call(context.querySelectorAll('input, select, textarea'), 0),
        input,
        name,
        isCheckbox,
        i;
    if (_lastClickedButtonId) {
        inputs.push(document.getElementById(_lastClickedButtonId));
    }
    for (i = inputs.length - 1; i > -1; i--) {
        input = inputs[i];
        if (null != input && !_ignore(input)) {
            isCheckbox = input.type == 'checkbox' || input.type == 'radio';
            if (!isCheckbox || input.checked) {
                name = input.name;
                if (name) {
                    if (!buffer[name]) {
                        buffer[name] = [];
                    }
                    buffer[name].push(input.value);
                }
            }
        }
    }
    return collection;
}

function _validateGroup(group) {
    var result = {
            invalidInputs: {},
            invalidCount: 0
        },
        inputs = group.querySelectorAll('input, select, textarea'),
        i;
    for (i = inputs.length - 1; i > -1; i--) {
        if (inputs[i].$validate(null, null, result.invalidInputs)) {
            result.invalidCount++;
            inputs[i].removeEventListener('change', _onchange_invalidInput, false);
            inputs[i].addEventListener('change', _onchange_invalidInput, false);
        }
    }
    return result;
}

function _collectEquivalentGroups(form) {
    var inputGroups = form.querySelectorAll('[data-validation-group]'),
        result = {},
        inputGroup,
        inputGroupName,
        i,
        j;
    for (i = 0, j = inputGroups.length; i < j; i++) {
        inputGroup = inputGroups[i];
        inputGroupName = inputGroup.getAttribute('data-validation-group') || rfm.guid();
        if (!result[inputGroupName]) {
            result[inputGroupName] = [];
        }
        result[inputGroupName].push(inputGroup);
    }
}

function _checkEquivalentGroups(inputGroups, outValues, outMessages) {
    var validInputGroup = null,
        inputGroup = inputGroups[0],
        lastFocus = inputGroup.getAttribute('data-last-focus') || Number.MIN_VALUE,
        otherLastFocus,
        groupValidity,
        otherValidity,
        inputName,
        i,
        j;
    for (i = 1, j = inputGroups.length; i < j; i++) {
        otherLastFocus = inputGroups[i].getAttribute('data-last-focus');
        if (otherLastFocus > lastFocus) {
            inputGroup.setAttribute('data-validated', 'false');
            inputGroup = inputGroups[i];
        }
        else {
            inputGroups.setAttribute('data-validated', 'false');
        }
    }
    inputGroup.setAttribute('data-validated', 'true');
    groupValidity = _validateGroup(inputGroup);
    if (groupValidity.invalidCount < 1) {
        validInputGroup = inputGroup;
    }
    else {
        for (i = 0, j = inputGroups.length; i < j; i++) {
            otherValidity = _validateGroup(inputGroups[i]);
            if (otherValidity.invalidCount < 1) {
                validInputGroup = inputGroups[i];
                break;
            }
        }
    }
    if (validInputGroup) {
        _collectValues(validInputGroup, outValues);
        return true;
    }
    else {
        for (inputName in groupValidity.invalidInputs) {
            outMessages[inputName] = groupValidity.invalidInputs[inputName];
        }
        return false;
    }
}

function _onclick_button(e) {
    if (e.target.nodeName == 'BUTTON') {
        if (!e.target.id) {
            e.target.id = 'button-' + _guid++;
        }
        _lastClickedButtonId = e.target.id;
    }
}

function _onchange_invalidInput(e) {
    if (!rfm.Form.isInvalid(this)) {
	    this.removeEventListener('change', arguments.callee, false);
    }
}

function _onfocus(e) {
    if (e.target.nodeType == 1 && (e.target.nodeName == 'INPUT' || e.target.nodeName == 'SELECT' || e.target.nodeName == 'TEXTAREA')) {
        var node = e.target.parentNode,
            group;
        while (node) {
            if (node.nodeType == 1) {
                if (node.nodeName == 'FORM') {
                    break;
                }
                else if (node.getAttribute('data-validation-group')) {
                    group = node;
                    break;
                }
            }
            node = node.parentNode;
        }
        (group || e.target).setAttribute('data-last-focus', new Date().getTime());
    }
}

/**
 * Utility for working with forms
 */
rfm.Form = {
    /**
     * Adds a test that will be run against form inputs
     * @param {Function(Element)=>Boolean} test A function that accepts the input element as an argument and returns true when the input is valid or false when the input is invalid
     * @param {Function(Element,String)=>String} message A function that accepts the input element and its label as arguments and returns a message for when the input failed validation
     */
    addValidator: function(test, message) {
        _validators[_validators.length] = {
            test: test,
            message: message
        };
    },
    /**
     * Regular expressions against which string values can be checked
     */
    patterns: {
        email: /^[A-Z0-9\.\_\%\+\-]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}$/i,
        number: /^\d*\.?\d+$/,
        tel: /^(\+?\d+( |-|\.)?)?\(?\d{3}\)?( |-|\.)?\d{3}( |-|\.)?\d{4}$/
    },
    /**
     * Messages returned when a validity test is failed
     */
    messages: {
        required: '{0} is required',
        confirmRequired: 'You must select "{0}"',
        selectionRequired: 'You must select an option',
        matches: '{0} must be the same as {1}',
        email: '{0} must be a valid email address',
        number: '{0} must be a number',
        min: '{0} may not be less than {1}',
        max: '{0} may not be more than {1}',
        tel: '{0} must be a valid phone number',
        pattern: '{0} is invalid'
    }
};

/**
 * Determines whether or not the user has either entered a value or selected a value for this form element
 * @returns {Boolean} True if the form element has entered or selected a value, false otherwise
 */
HTMLSelectElement.prototype.$hasValue =
HTMLTextAreaElement.prototype.$hasValue =
HTMLInputElement.prototype.$hasValue = function() {
	if (this.type == 'radio' || this.type == 'checkbox') {
		return this.checked;
	}
	else {
		return !!this.value;
	}
};

/**
 * Gets the label for an input
 * @returns {String} That input's label
 */
HTMLInputElement.prototype.$label = 
HTMLTextAreaElement.prototype.$label = 
HTMLSelectElement.prototype.$label = function() {
    if (this.id) {
        var label = document.querySelector('label[for="' + this.id + '"]');
        if (label) {
            return label.textContent;
        }
    }
    return '';
};

/**
 * Checks whether or not an input is invalid
 * @param {Function()} onvalid (Optional) A callback to execute when the input is valid
 * @param {Function(String)} oninvalid (Optional) A callback to execute when the form is invalid, passed a validation message
 * @returns {Boolean} True when the input is valid or inactive, false otherwise
 */
HTMLInputElement.prototype.$validate =
HTMLTextAreaElement.prototype.$validate =
HTMLSelectElement.prototype.$validate = function(onvalid, oninvalid) {
    var collection = arguments.length > 2 ? arguments[2] : {},
        message = false,
        label = this.$label(),
        inputType = this.getAttribute('type'),
        dependsOn = this.getAttribute('data-depends-on'),
        isActive = !_ignore(this),
        isRequired = false,
        isCheckbox = inputType == 'checkbox',
        isRadio = inputType == 'radio',
        inputValue = this.value,
        hasValue,
        o;
    if (isActive && dependsOn) {
        dependsOn = dependsOn.split(',');
        for (o in dependsOn) {
            isActive = isActive && document.getElementById(dependsOn[o]).$hasValue();
        }
    }
    if (isActive) {
        hasValue = this.$hasValue();
        isRequired = this.getAttribute('required') !== null;
        if (!isRadio && !isCheckbox && isRequired && !hasValue) {
            message = rfm.format(rfm.Form.messages.required, label);
        }
        else if ((isRadio || isCheckbox) && isRequired) {
            o = _radioCheckListSelection(this);
            if (!o.hasSelection) {
                message = rfm.format(o.oneOption ? rfm.Form.messages.confirmRequired : rfm.Form.messages.selectionRequired, label);
            }
        }
        else if (hasValue && (o = this.getAttribute('data-matches')) && (o = document.getElementById(o)).value != inputValue) {
            message = rfm.format(rfm.Form.messages.matches, label, o.$label());
        }
        else if (hasValue && this.type == 'email' && !rfm.Form.patterns.email.test(inputValue)) {
            message = rfm.format(rfm.Form.messages.email, label);
        }
        else if (hasValue && inputType == 'number') {
            if (!rfm.Form.patterns.number.test(inputValue)) {
                message = rfm.format(rfm.Form.messages.number, label);
            }
            else {
                o = parseFloat(inputValue);
                var min = this.getAttribute('min'),
                    max = this.getAttribute('max');
                if (min && o < parseFloat(min)) {
                    message = rfm.format(rfm.Form.messages.min, label, min);
                }
                else if (max && o > parseFloat(max)) {
                    message = rfm.format(rfm.Form.messages.max, label, max);
                }
            }
        }
        else if (hasValue && inputType == 'tel' && !rfm.Form.patterns.tel.test(inputValue)) {
			message = rfm.format(rfm.Form.messages.phone, label);
		}
		if (hasValue && (o = this.getAttribute('pattern')) && !new RegExp(o).test(inputValue)) {
		    message = rfm.format(rfm.Form.messages.pattern, label);
		}
		for (var i = 0, j = _validators.length; i < j; i++) {
		    var validator = _validators[i];
		    if (!validator.test(this)) {
		        message = validator.message(this, label);
		        break;
		    }
		}
		if (message) {
		    collection[this.name] = message;
		}
		this.setAttribute('aria-invalid', message ? 'true' : 'false');
    }
    if (message) {
        if (onvalid) {
            onvalid.call(this);
        }
    }
    else {
        if (oninvalid) {
            oninvalid.call(this, message);
        }
    }
	return !message;
};

/**
 * Check whether or not the form is valid and execute callbacks
 * @param {Function(Object<String,Array<String>>)} onvalid (Optional) A callback to execute when the form is valid, passed a hash of form input names and a list of their values
 * @param {Function(Object<String,String>)} oninvalid (Optional) A callback to execute when the form is invalid, passed a hash of form input names and their validation messages
 * @returns {Boolean} True if the form was valid, false otherwise
 */
HTMLFormElement.prototype.$validate = function(onvalid, oninvalid) {
    var okToSubmit = true,
        equivGroups = _collectEquivalentGroups(this),
        messages = {},
        values = {},
        inputGroupName;
    for (inputGroupName in equivGroups) {
        okToSubmit = okToSubmit && _checkEquivalentGroups(equivGroups[inputGroupName], values, messages);
    }
    if (e) {
        e.preventDefault();
    }
    _lastClickedButtonId = null;
    if (okToSubmit) {
        if (onvalid) {
            onvalid.call(form, values);
        }
    }
    else {
        if (oninvalid) {
            oninvalid.call(form, messages);
        }
    }
    return okToSubmit;
};

/**
 * Attaches bindings to the form that monitor validation groups and button input
 * @returns {HTMLFormElement} The form
 */
HTMLFormElement.prototype.$initValidationHelpers = function() {
    this.removeEventListener('focus', _onfocus, true);
    this.removeEventListener('change', _onfocus, true);
    this.removeEventListener('click', _onclick_button, false);
    this.addEventListener('focus', _onfocus, true);
    this.addEventListener('change', _onfocus, true);
    this.addEventListener('click', _onclick_button, false);
    return this;
};

})();